import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { IgniteContracts } from "../target/types/ignite_contracts";
import { PublicKey, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";
import { 
  TOKEN_PROGRAM_ID, 
  ASSOCIATED_TOKEN_PROGRAM_ID,
  getAssociatedTokenAddress,
  createAssociatedTokenAccountInstruction,
} from "@solana/spl-token";
import { assert } from "chai";

describe("ignite-contracts", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());
  const provider = anchor.getProvider();
  const program = anchor.workspace.IgniteContracts as Program<IgniteContracts>;

  // Test accounts
  let factoryPda: PublicKey;
  let factoryBump: number;
  let bondingCurvePda: PublicKey;
  let bondingCurveBump: number;
  let mintKeypair: Keypair;
  let creator: Keypair;
  let buyer: Keypair;

  before(async () => {
    // Generate test accounts
    creator = Keypair.generate();
    buyer = Keypair.generate();
    mintKeypair = Keypair.generate();

    // Airdrop SOL to test accounts
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(creator.publicKey, 2 * LAMPORTS_PER_SOL),
      "confirmed"
    );
    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(buyer.publicKey, 2 * LAMPORTS_PER_SOL),
      "confirmed"
    );

    // Find PDAs
    [factoryPda, factoryBump] = await PublicKey.findProgramAddress(
      [Buffer.from("factory")],
      program.programId
    );

    [bondingCurvePda, bondingCurveBump] = await PublicKey.findProgramAddress(
      [Buffer.from("curve"), mintKeypair.publicKey.toBuffer()],
      program.programId
    );
  });

  it("Initialize factory", async () => {
    try {
      const tx = await program.methods
        .initializeFactory()
        .accounts({
          factory: factoryPda,
          authority: provider.wallet.publicKey,
          systemProgram: anchor.web3.SystemProgram.programId,
        })
        .rpc();

      console.log("Factory initialized, tx:", tx);

      // Verify factory state
      const factoryAccount = await program.account.factory.fetch(factoryPda);
      assert.equal(factoryAccount.authority.toString(), provider.wallet.publicKey.toString());
      assert.equal(factoryAccount.totalTokens.toNumber(), 0);
      assert.equal(factoryAccount.graduationThreshold.toNumber(), 69_000 * LAMPORTS_PER_SOL);
    } catch (error) {
      console.log("Factory might already be initialized:", error.message);
    }
  });

  it("Create token with bonding curve", async () => {
    const name = "Test Token";
    const symbol = "TEST";
    const uri = "https://example.com/metadata.json";
    const totalSupply = new anchor.BN(1_000_000_000 * 1_000_000_000); // 1B tokens with 9 decimals

    const tx = await program.methods
      .createToken(name, symbol, uri, totalSupply)
      .accounts({
        factory: factoryPda,
        bondingCurve: bondingCurvePda,
        mint: mintKeypair.publicKey,
        creator: creator.publicKey,
        tokenProgram: TOKEN_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
      })
      .signers([creator, mintKeypair])
      .rpc();

    console.log("Token created, tx:", tx);

    // Verify bonding curve state
    const curveAccount = await program.account.bondingCurve.fetch(bondingCurvePda);
    assert.equal(curveAccount.mint.toString(), mintKeypair.publicKey.toString());
    assert.equal(curveAccount.creator.toString(), creator.publicKey.toString());
    assert.equal(curveAccount.totalSupply.toString(), totalSupply.toString());
    assert.equal(curveAccount.circulatingSupply.toNumber(), 0);
    assert.equal(curveAccount.solReserves.toNumber(), 0);
    assert.equal(curveAccount.isGraduated, false);

    console.log("Bonding curve state:", {
      mint: curveAccount.mint.toString(),
      creator: curveAccount.creator.toString(),
      totalSupply: curveAccount.totalSupply.toString(),
      circulatingSupply: curveAccount.circulatingSupply.toString(),
      solReserves: curveAccount.solReserves.toString(),
      isGraduated: curveAccount.isGraduated,
    });
  });

  it("Buy tokens from bonding curve", async () => {
    const solAmount = new anchor.BN(0.1 * LAMPORTS_PER_SOL); // 0.1 SOL

    // Get buyer's associated token account
    const buyerTokenAccount = await getAssociatedTokenAddress(
      mintKeypair.publicKey,
      buyer.publicKey
    );

    // Get initial balances
    const initialBuyerSol = await provider.connection.getBalance(buyer.publicKey);
    const initialCurveAccount = await program.account.bondingCurve.fetch(bondingCurvePda);

    console.log("Initial state:", {
      buyerSol: initialBuyerSol / LAMPORTS_PER_SOL,
      curveSupply: initialCurveAccount.circulatingSupply.toString(),
      curveReserves: initialCurveAccount.solReserves.toNumber() / LAMPORTS_PER_SOL,
    });

    const tx = await program.methods
      .buy(solAmount)
      .accounts({
        factory: factoryPda,
        bondingCurve: bondingCurvePda,
        mint: mintKeypair.publicKey,
        buyerTokenAccount: buyerTokenAccount,
        buyer: buyer.publicKey,
        tokenProgram: TOKEN_PROGRAM_ID,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
      })
      .signers([buyer])
      .rpc();

    console.log("Buy transaction:", tx);

    // Verify purchase
    const finalCurveAccount = await program.account.bondingCurve.fetch(bondingCurvePda);
    const buyerTokenBalance = await provider.connection.getTokenAccountBalance(buyerTokenAccount);

    console.log("After buy:", {
      tokensReceived: buyerTokenBalance.value.amount,
      curveSupply: finalCurveAccount.circulatingSupply.toString(),
      curveReserves: finalCurveAccount.solReserves.toNumber() / LAMPORTS_PER_SOL,
    });

    // Verify state changes
    assert.isTrue(finalCurveAccount.circulatingSupply.gt(initialCurveAccount.circulatingSupply));
    assert.isTrue(finalCurveAccount.solReserves.gt(initialCurveAccount.solReserves));
    assert.isTrue(parseInt(buyerTokenBalance.value.amount) > 0);
  });

  it("Sell tokens to bonding curve", async () => {
    // Get buyer's token account balance
    const buyerTokenAccount = await getAssociatedTokenAddress(
      mintKeypair.publicKey,
      buyer.publicKey
    );
    const tokenBalance = await provider.connection.getTokenAccountBalance(buyerTokenAccount);
    const sellAmount = new anchor.BN(parseInt(tokenBalance.value.amount) / 2); // Sell half

    // Get initial states
    const initialBuyerSol = await provider.connection.getBalance(buyer.publicKey);
    const initialCurveAccount = await program.account.bondingCurve.fetch(bondingCurvePda);

    console.log("Before sell:", {
      buyerSol: initialBuyerSol / LAMPORTS_PER_SOL,
      sellAmount: sellAmount.toString(),
      curveSupply: initialCurveAccount.circulatingSupply.toString(),
      curveReserves: initialCurveAccount.solReserves.toNumber() / LAMPORTS_PER_SOL,
    });

    const tx = await program.methods
      .sell(sellAmount)
      .accounts({
        factory: factoryPda,
        bondingCurve: bondingCurvePda,
        mint: mintKeypair.publicKey,
        sellerTokenAccount: buyerTokenAccount,
        seller: buyer.publicKey,
        tokenProgram: TOKEN_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .signers([buyer])
      .rpc();

    console.log("Sell transaction:", tx);

    // Verify sale
    const finalBuyerSol = await provider.connection.getBalance(buyer.publicKey);
    const finalCurveAccount = await program.account.bondingCurve.fetch(bondingCurvePda);
    const finalTokenBalance = await provider.connection.getTokenAccountBalance(buyerTokenAccount);

    console.log("After sell:", {
      buyerSol: finalBuyerSol / LAMPORTS_PER_SOL,
      tokensRemaining: finalTokenBalance.value.amount,
      curveSupply: finalCurveAccount.circulatingSupply.toString(),
      curveReserves: finalCurveAccount.solReserves.toNumber() / LAMPORTS_PER_SOL,
    });

    // Verify state changes
    assert.isTrue(finalCurveAccount.circulatingSupply.lt(initialCurveAccount.circulatingSupply));
    assert.isTrue(finalCurveAccount.solReserves.lt(initialCurveAccount.solReserves));
    assert.isTrue(finalBuyerSol > initialBuyerSol - 10000); // Account for transaction fees
  });

  it("Test with existing mint", async () => {
    // Test with your existing mint
    const existingMint = new PublicKey("132XhFBhSzW1DDLu6sD5qvVnpjxfWpD5o4MJgBAvVtXi");
    
    // Find the bonding curve PDA for existing mint
    const [existingCurvePda] = await PublicKey.findProgramAddress(
      [Buffer.from("curve"), existingMint.toBuffer()],
      program.programId
    );

    try {
      // Try to fetch the bonding curve account
      const curveAccount = await program.account.bondingCurve.fetch(existingCurvePda);
      
      console.log("Existing mint bonding curve:", {
        mint: curveAccount.mint.toString(),
        creator: curveAccount.creator.toString(),
        totalSupply: curveAccount.totalSupply.toString(),
        circulatingSupply: curveAccount.circulatingSupply.toString(),
        solReserves: curveAccount.solReserves.toNumber() / LAMPORTS_PER_SOL,
        isGraduated: curveAccount.isGraduated,
        createdAt: new Date(curveAccount.createdAt.toNumber() * 1000).toISOString(),
      });

      // Test buying with existing mint
      const solAmount = new anchor.BN(0.01 * LAMPORTS_PER_SOL); // 0.01 SOL
      const buyerTokenAccount = await getAssociatedTokenAddress(
        existingMint,
        buyer.publicKey
      );

      console.log("\nTesting buy with existing mint...");
      const buyTx = await program.methods
        .buy(solAmount)
        .accounts({
          factory: factoryPda,
          bondingCurve: existingCurvePda,
          mint: existingMint,
          buyerTokenAccount: buyerTokenAccount,
          buyer: buyer.publicKey,
          tokenProgram: TOKEN_PROGRAM_ID,
          associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
          systemProgram: anchor.web3.SystemProgram.programId,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        })
        .signers([buyer])
        .rpc();

      console.log("Buy transaction with existing mint:", buyTx);

      // Check updated state
      const updatedCurveAccount = await program.account.bondingCurve.fetch(existingCurvePda);
      console.log("Updated state:", {
        circulatingSupply: updatedCurveAccount.circulatingSupply.toString(),
        solReserves: updatedCurveAccount.solReserves.toNumber() / LAMPORTS_PER_SOL,
      });

    } catch (error) {
      console.log("Error testing existing mint:", error.message);
      console.log("This might mean the bonding curve doesn't exist for this mint yet.");
    }
  });
});
